16. 보안의 기본과 개념2 심화 - TLS
SSL/TLS 개요
SSL/TLS란?
암호화 통신을 수행하는 절차를 규정한 프로토콜. 공개키 암호화 방식을 이용하여 연결 대상의 정당성을 확인한 후 암호화 통신에 필요한 키를 생성하고, 패킷 암호화와 복호화를 실행함
역사적 발전
SSL/TLS 발전 과정:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[1990년대 중반]
SSL 1.0 (공개 안 됨)
↓
SSL 2.0 (Netscape)
↓
SSL 3.0 (1996)
↓
[1999년]
TLS 1.0 (SSL 3.1)
↓
[2006년]
TLS 1.1
↓
[2008년]
TLS 1.2 (현재 널리 사용)
↓
[2018년]
TLS 1.3 (최신 표준)
→ SSL 3.0, TLS 1.0, 1.1: 취약점 발견
→ TLS 1.2, 1.3: 권장 프로토콜
프로토콜 배경:
- SSL과 TLS는 모두 웹 브라우저와 웹 서버 간 통신을 암호화하는 프로토콜
- HTTPS 프로토콜에는 암호화를 위한 상세한 절차를 규정하지 않아 웹 브라우저에 구현된 SSL 프로토콜이 사용됨
- SSL은 웹 브라우저 개발사(Netscape)에서 만든 프로토콜
- 인터넷 기술 표준화 단체인 IETF가 TLS로 재설계하여 인터넷 표준으로 채택
- TLS는 SSL을 계승하여 설계되었지만 엄격한 호환성은 없음
TLS 핸드셰이크 완전 분석
TLS 핸드셰이크란?
TLS 통신을 시작하기 전에 클라이언트와 서버가:
- 서로 인증하고
- 암호화 방식을 협상하며
- 안전하게 세션 키를 교환하는 과정
전체 과정 (TLS 1.2 기준)
TLS 핸드셰이크 9단계:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[클라이언트] [서버]
| |
|--① ClientHello----------------→|
| |
| 전송 내용: |
| • 지원하는 TLS 버전 |
| (TLS 1.2, TLS 1.3 등) |
| • 지원하는 암호화 알고리즘 목록 |
| (AES-256-GCM, RSA-2048 등) |
| • 클라이언트 랜덤 (32바이트) |
| "abc123..." |
| • 세션 ID (재사용 여부) |
| |
|←-② ServerHello------------------|
| |
| 전송 내용: |
| • 선택한 TLS 버전 |
| • 선택한 암호화 알고리즘 |
| • 서버 랜덤 (32바이트) |
| "xyz789..." |
| • 세션 ID |
| |
|←-③ Certificate------------------|
| |
| 전송 내용: |
| • 서버 인증서 (X.509 형식) |
| - 서버 도메인 정보 |
| - 서버 공개키 |
| - CA의 전자 서명 |
| - 유효 기간 |
| |
|←-④ ServerHelloDone--------------|
| |
| 의미: 서버 메시지 전송 완료 |
| |
↓ |
[인증서 검증 단계] |
| |
├─ CA 서명 확인 |
│ (브라우저 내장 CA 리스트 확인) |
├─ 유효 기간 확인 |
├─ 도메인 일치 확인 |
└─ 폐기 여부 확인 (OCSP/CRL) |
| |
|--⑤ ClientKeyExchange----------→|
| |
| 전송 내용: |
| • Pre-Master Secret (48바이트) |
| 클라이언트가 생성한 랜덤 값 |
| • 서버 공개키(RSA)로 암호화됨 |
| "8f3e9d2a..." (암호화된 상태) |
| |
| ↓
| [개인키로 복호화]
| |
| Pre-Master Secret 획득
| |
↓ ↓
[세션 키 생성] [세션 키 생성]
| |
재료: 재료:
• 클라이언트 랜덤 "abc123..." • 클라이언트 랜덤 "abc123..."
• 서버 랜덤 "xyz789..." • 서버 랜덤 "xyz789..."
• Pre-Master Secret "pms456..." • Pre-Master Secret "pms456..."
| |
↓ ↓
Master Secret 생성: Master Secret 생성:
PRF(pms456..., "master secret", PRF(pms456..., "master secret",
abc123... + xyz789...) abc123... + xyz789...)
| |
↓ ↓
"master789..." "master789..."
| |
↓ ↓
세션 키들 생성: 세션 키들 생성:
PRF(master789..., "key expansion", PRF(master789..., "key expansion",
xyz789... + abc123...) xyz789... + abc123...)
| |
↓ ↓
• client_write_encryption_key • client_write_encryption_key
• server_write_encryption_key • server_write_encryption_key
• client_write_MAC_key • client_write_MAC_key
• server_write_MAC_key • server_write_MAC_key
| |
|--⑥ ChangeCipherSpec-----------→|
| |
| 의미: "이제부터 암호화 시작!" |
| |
|--⑦ Finished-------------------→|
| |
| 내용: |
| • 지금까지 교환한 모든 메시지의 |
| 해시값 (암호화됨) |
| • 서버가 이걸 검증함 |
| |
| ↓
| [Finished 메시지 검증]
| |
|←-⑧ ChangeCipherSpec-------------|
| |
| 의미: "OK, 암호화 시작!" |
| |
|←-⑨ Finished---------------------|
| |
| 내용: |
| • 지금까지 교환한 모든 메시지의 |
| 해시값 (암호화됨) |
| • 클라이언트가 이걸 검증함 |
| |
↓ ↓
[Finished 메시지 검증] |
| |
|←======암호화 통신 시작========→|
| |
| 이제부터 모든 데이터는: |
| • AES-256으로 암호화됨 |
| • MAC으로 무결성 검증됨 |
| |
각 단계 상세 설명
① ClientHello
클라이언트가 서버에게 "안녕, 연결하고 싶어요" 하며 자신의 능력을 알림
ClientHello 메시지 구조:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{
"version": "TLS 1.2",
"random": "abc123...", (32바이트)
"session_id": "",
"cipher_suites": [
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_CBC_SHA256",
...
],
"compression_methods": ["null"],
"extensions": {
"server_name": "example.com",
"supported_groups": ["x25519", "secp256r1"],
"signature_algorithms": ["rsa_pss_rsae_sha256"]
}
}
② ServerHello
서버가 "OK, 이 방식으로 하자" 하며 하나를 선택
ServerHello 메시지 구조:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{
"version": "TLS 1.2",
"random": "xyz789...", (32바이트)
"session_id": "session123...",
"cipher_suite": "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"compression_method": "null"
}
→ 클라이언트가 제안한 목록 중 하나를 선택
③ Certificate
서버가 "내가 진짜야" 하며 신분증(인증서) 제시
⑤ ClientKeyExchange
핵심! Pre-Master Secret을 안전하게 전달
Pre-Master Secret 생성:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[클라이언트가 생성]
48바이트 랜덤 값
↓
0x03 0x03 (TLS 버전) + 46바이트 랜덤
↓
서버 공개키(RSA)로 암호화
↓
암호화된 Pre-Master Secret 전송
[서버가 수신]
↓
서버 개인키(RSA)로 복호화
↓
Pre-Master Secret 획득
→ 중간자는 개인키가 없어서 복호화 불가!
⑦, ⑨ Finished 메시지
지금까지의 모든 핸드셰이크 메시지가 변조되지 않았는지 최종 확인
Finished 메시지 검증:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
verify_data = PRF(
master_secret,
"client finished" 또는 "server finished",
SHA256(모든 핸드셰이크 메시지)
)[12바이트]
→ 양쪽이 계산한 값이 일치하면 성공!
→ 중간자가 메시지를 변조했다면 불일치
인증서 검증 과정
클라이언트가 수행하는 4단계 검증
서버 인증서 검증:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[서버 인증서 수신]
↓
1단계: CA 서명 검증
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
↓
브라우저에 내장된 신뢰할 수 있는
CA(인증 기관) 리스트 확인
↓
예시: DigiCert, Let's Encrypt, Comodo 등
↓
인증서의 발급자(Issuer) 확인
↓
해당 CA의 공개키로 인증서의 서명 검증
↓
서명이 유효한가?
↓
YES → ✓ CA가 정말 발급한 인증서
NO → ✗ 위조된 인증서
2단계: 유효 기간 확인
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
↓
인증서의 Not Before와 Not After 확인
↓
예시:
Not Before: 2024-01-01 00:00:00 UTC
Not After: 2025-12-31 23:59:59 UTC
↓
현재 시간이 이 범위 내에 있는가?
↓
YES → ✓ 유효 기간 내
NO → ✗ 만료됨 또는 아직 유효하지 않음
3단계: 도메인 일치 확인
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
↓
사용자가 접속하려는 도메인:
https://example.com
↓
인증서의 Common Name (CN) 또는
Subject Alternative Name (SAN) 확인:
CN: example.com
SAN: example.com, www.example.com, *.example.com
↓
도메인이 일치하는가?
↓
YES → ✓ 올바른 서버의 인증서
NO → ✗ 다른 서버의 인증서 (피싱 의심)
4단계: 폐기 여부 확인
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
↓
인증서가 중간에 폐기되었는지 확인
↓
방법 1: CRL (Certificate Revocation List)
• CA가 제공하는 폐기 목록 다운로드
• 인증서 일련번호가 목록에 있는지 확인
• 단점: 목록이 크고 느림
↓
방법 2: OCSP (Online Certificate Status Protocol)
• CA에 실시간으로 질의
• 응답: Good / Revoked / Unknown
• 단점: CA 서버 부하, 프라이버시 문제
↓
방법 3: OCSP Stapling
• 서버가 주기적으로 OCSP 응답 받아옴
• 클라이언트에게 인증서와 함께 전송
• 장점: 빠르고 프라이버시 보호
↓
폐기되지 않았는가?
↓
YES → ✓ 유효한 인증서
NO → ✗ 폐기됨 (보안 사고 가능성)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
모든 검증 통과 시:
✓ 서버 인증 성공
✓ 공개키 신뢰 가능
✓ 핸드셰이크 계속 진행
하나라도 실패 시:
✗ 연결 중단
✗ 경고 메시지 표시
인증서 체인 검증
인증서 체인 (Certificate Chain):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[루트 CA]
DigiCert Global Root CA
└─ 브라우저에 사전 설치됨
└─ 자체 서명 (Self-signed)
|
| (서명)
↓
[중간 CA]
DigiCert SHA2 Secure Server CA
└─ 루트 CA가 서명함
|
| (서명)
↓
[서버 인증서]
example.com
└─ 중간 CA가 서명함
검증 과정:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 서버 인증서의 서명을 중간 CA 공개키로 검증
2. 중간 CA 인증서의 서명을 루트 CA 공개키로 검증
3. 루트 CA는 브라우저에 내장되어 신뢰됨
→ 체인이 끊기지 않고 루트 CA까지 연결되면 신뢰!
하이브리드 암호화 시스템
왜 두 가지 암호화를 함께 사용하나?
암호화 방식별 장단점:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[공개키 암호화 (RSA, ECC)]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
장점:
✓ 키 교환 안전
✓ 사전 키 공유 불필요
✓ 전자 서명 가능
단점:
✗ 속도 매우 느림 (1000배 이상)
✗ 복잡한 수학 연산
✗ CPU 부하 높음
성능:
RSA-2048로 1MB 암호화: ~10초
용도:
→ 세션 키 교환에만 사용
[대칭키 암호화 (AES)]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
장점:
✓ 속도 매우 빠름
✓ 간단한 연산
✓ CPU 부하 낮음
✓ 하드웨어 가속 지원
단점:
✗ 키 교환 어려움
✗ 사전 키 공유 필요
✗ 전자 서명 불가
성능:
AES-256으로 1MB 암호화: ~0.01초
용도:
→ 실제 데이터 암호화에 사용
TLS의 하이브리드 방식
하이브리드 암호화 흐름:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase 1: 핸드셰이크 (공개키 암호화)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
목적: 세션 키를 안전하게 교환
[클라이언트]
↓
1. Pre-Master Secret 생성
48바이트 랜덤 값: "pms456..."
↓
2. 서버 공개키(RSA)로 암호화
RSA-2048 암호화
걸리는 시간: ~0.01초
↓
3. 암호화된 값 전송
"8f3e9d2a..." (256바이트)
↓
[서버]
↓
1. 서버 개인키(RSA)로 복호화
RSA-2048 복호화
걸리는 시간: ~0.1초
↓
2. Pre-Master Secret 획득
"pms456..."
→ 양쪽 모두 Pre-Master Secret 보유
→ 같은 알고리즘으로 세션 키(AES) 생성
특징:
• 느리지만 안전한 키 교환
• 단 한 번만 수행 (핸드셰이크 시)
• 이후에는 사용 안 함
Phase 2: 데이터 전송 (대칭키 암호화)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
목적: 빠른 속도로 대용량 데이터 암호화
[클라이언트] → [서버]
↓
HTTP 요청:
"GET /api/data HTTP/1.1
Host: example.com
..."
↓
AES-256-GCM으로 암호화
걸리는 시간: ~0.0001초
↓
암호화된 데이터 전송
[서버] → [클라이언트]
↓
JSON 응답:
"{data: [...]}" (1MB)
↓
AES-256-GCM으로 암호화
걸리는 시간: ~0.01초
↓
암호화된 데이터 전송
특징:
• 매우 빠른 암호화/복호화
• 모든 애플리케이션 데이터에 사용
• 세션 동안 지속적으로 사용
실제 HTTPS 통신 예시
실제 웹 페이지 로딩:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[1] TCP 3-way handshake
시간: ~50ms
↓
[2] TLS 핸드셰이크 (RSA 사용)
• ClientHello
• ServerHello + Certificate
• ClientKeyExchange (RSA 암호화)
• ChangeCipherSpec + Finished
시간: ~200ms
↓
[3] HTTP 요청 (AES 암호화)
GET /index.html HTTP/1.1
크기: 500 bytes
암호화 시간: 0.0001ms
전송 시간: ~10ms
↓
[4] HTTP 응답 (AES 암호화)
HTML + CSS + JS
크기: 500KB
암호화 시간: 5ms
전송 시간: ~500ms
↓
[5] 추가 리소스 요청 (AES 암호화)
이미지 10개 (각 100KB)
각 암호화 시간: 1ms
전송 시간: ~1000ms
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
총 소요 시간:
• 핸드셰이크 (RSA): 200ms (한 번만)
• 데이터 전송 (AES): 1510ms (지속적)
만약 RSA만 사용했다면:
• 핸드셰이크: 200ms
• 데이터 전송: 150,000ms (100배 느림!)
→ 하이브리드 방식으로 150초 → 1.7초로 단축!
세션 키 생성 메커니즘
양쪽이 같은 키를 만드는 방법
세션 키 생성 재료:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[클라이언트] [서버]
| |
재료 1: 클라이언트 랜덤 재료 1: 클라이언트 랜덤
"abc123..." (32바이트) "abc123..." (받음)
| |
재료 2: 서버 랜덤 재료 2: 서버 랜덤
"xyz789..." (받음) "xyz789..." (32바이트)
| |
재료 3: Pre-Master Secret 재료 3: Pre-Master Secret
"pms456..." (생성) "pms456..." (복호화로 획득)
| |
└────────────┬───────────────┘
↓
양쪽 모두 같은 재료 보유!
Master Secret 생성 (1단계)
Master Secret 생성:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
입력:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• secret: pre_master_secret "pms456..."
• label: "master secret" (ASCII 문자열)
• seed: client_random + server_random
"abc123..." + "xyz789..."
처리:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
master_secret = PRF(
secret: "pms456...",
label: "master secret",
seed: "abc123..." + "xyz789..."
)
PRF 내부 동작:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
A(0) = seed = "abc123xyz789..."
A(1) = HMAC-SHA256(secret, A(0))
= HMAC-SHA256("pms456...", "abc123xyz789...")
= "hash1..."
P(1) = HMAC-SHA256(secret, A(1) + label + seed)
= HMAC-SHA256("pms456...",
"hash1..." + "master secret" + "abc123xyz789...")
= "result1..."
A(2) = HMAC-SHA256(secret, A(1))
= "hash2..."
P(2) = HMAC-SHA256(secret, A(2) + label + seed)
= "result2..."
master_secret = P(1) + P(2) [처음 48바이트]
출력:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
master_secret = "ms789abc..." (48바이트)
→ 클라이언트와 서버가 동일한 값 생성!
Key Block 생성 (2단계)
Key Block 생성 (여러 키들):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
입력:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• secret: master_secret "ms789abc..."
• label: "key expansion" (ASCII 문자열)
• seed: server_random + client_random (순서 주의!)
"xyz789..." + "abc123..."
처리:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
key_block = PRF(
secret: "ms789abc...",
label: "key expansion",
seed: "xyz789..." + "abc123..." ← 순서 바뀜!
)
출력:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
key_block = "kb123def456ghi789jkl..." (충분히 긴 바이트 배열)
Key Block 분배:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
key_block을 정해진 순서대로 자르기:
바이트 [0-31]: client_write_MAC_key
(클라이언트→서버 무결성 검증용)
"mac_c_123..."
바이트 [32-63]: server_write_MAC_key
(서버→클라이언트 무결성 검증용)
"mac_s_456..."
바이트 [64-95]: client_write_encryption_key
(클라이언트→서버 암호화용)
"enc_c_789..."
바이트 [96-127]: server_write_encryption_key
(서버→클라이언트 암호화용)
"enc_s_abc..."
바이트 [128-143]: client_write_IV
(초기화 벡터)
"iv_c_def..."
바이트 [144-159]: server_write_IV
(초기화 벡터)
"iv_s_ghi..."
→ 양쪽이 완전히 동일한 키들을 얻음!
왜 이렇게 복잡한가?
보안을 위한 설계:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[랜덤성 증가]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
client_random + server_random 결합
↓
양쪽 모두 랜덤 값 제공
↓
어느 한쪽도 결과를 예측할 수 없음
↓
재료: "abc123..." + "xyz789..." = "abc123xyz789..."
↓
256비트 엔트로피
[다른 용도의 키 구분]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
같은 재료로 다른 키를 만들 때:
Master Secret 생성:
PRF(pms, "master secret", c_rand + s_rand)
Key Expansion:
PRF(ms, "key expansion", s_rand + c_rand)
→ 레이블과 순서를 바꿔서 구분!
→ 충돌 방지
[순서 바꾸기]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Master Secret:
client_random + server_random
"abc123xyz789..."
Key Block:
server_random + client_random
"xyz789abc123..."
→ 추가 엔트로피 확보
→ 패턴 공격 방어
[Perfect Forward Secrecy]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
각 세션마다:
• 새로운 client_random
• 새로운 server_random
• 새로운 pre_master_secret
↓
매번 다른 세션 키 생성!
↓
과거 세션 기록이 노출되어도
다른 세션은 안전
PRF와 레이블의 역할
PRF란?
PRF = Pseudo-Random Function:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
의사 난수 함수
• 결정적(Deterministic): 같은 입력 → 같은 출력
• 예측 불가능: 입력의 일부만 바뀌어도 완전히 다른 출력
• 일방향: 출력에서 입력을 역산 불가능
TLS 1.2의 PRF:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HMAC-SHA256 기반
레이블의 역할
레이블은 TLS RFC 표준에 정의된 고정 문자열
레이블(Label) = 구분용 상수:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
같은 재료로 다른 결과를 만들기 위한 장치
비유:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
재료: 밀가루 + 물 + 소금
"빵" + 재료 → 빵 반죽
"면" + 재료 → 면 반죽
"만두피" + 재료 → 만두피 반죽
→ 같은 재료지만 레이블에 따라 다른 결과!
TLS에서 사용되는 레이블들
TLS 1.2 RFC 5246에 정의된 레이블:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"master secret"
→ Master Secret 생성
→ PRF(pre_master_secret, "master secret", randoms)
"key expansion"
→ 세션 키들 생성
→ PRF(master_secret, "key expansion", randoms)
"client finished"
→ 클라이언트 Finished 메시지 검증
→ PRF(master_secret, "client finished", handshake_hash)
"server finished"
→ 서버 Finished 메시지 검증
→ PRF(master_secret, "server finished", handshake_hash)
→ 모두 RFC 문서에 정확히 명시됨
→ 전 세계 모든 TLS 구현체가 동일하게 사용
왜 레이블을 사용하나?
레이블이 필요한 이유:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
만약 레이블이 없다면?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRF(pre_master_secret, client_random + server_random)
→ 결과 A
PRF(master_secret, server_random + client_random)
→ 결과 B
문제:
• 순서만 바뀌어서 혼동 가능
• 같은 값이 다른 용도로 사용될 위험
• 보안 취약점 발생 가능
레이블을 사용하면?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRF(pre_master_secret,
"master secret" + client_random + server_random)
→ 결과 A
PRF(master_secret,
"key expansion" + server_random + client_random)
→ 결과 B
장점:
✓ 서로 다른 용도임을 명확히 구분
✓ 같은 입력으로 다른 키 생성 가능
✓ 충돌 방지 (키가 우연히 같아지는 것 방지)
✓ 보안 강화
✓ 표준화 (모든 구현체가 동일하게 동작)
실제 계산 예시
구체적인 예시 (단순화):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
재료:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
pre_master_secret = 0x1234ABCD...
client_random = 0xAAAA...
server_random = 0xBBBB...
1단계: Master Secret 만들기
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
label = "master secret"
ASCII 인코딩:
0x6D 0x61 0x73 0x74 0x65 0x72 0x20
0x73 0x65 0x63 0x72 0x65 0x74
seed = client_random + server_random
= 0xAAAA... + 0xBBBB...
HMAC 입력 = label + seed
= "master secret" + 0xAAAABBBB...
master_secret = PRF(0x1234ABCD..., HMAC 입력)
= 0x5678EFGH... (48바이트)
2단계: Key Block 만들기
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
label = "key expansion"
ASCII 인코딩:
0x6B 0x65 0x79 0x20 0x65 0x78 0x70
0x61 0x6E 0x73 0x69 0x6F 0x6E
seed = server_random + client_random (순서 바뀜!)
= 0xBBBB... + 0xAAAA...
HMAC 입력 = label + seed
= "key expansion" + 0xBBBBAAAA...
key_block = PRF(0x5678EFGH..., HMAC 입력)
= 0x9ABC1234... (필요한 만큼)
→ 이제 key_block을 잘라서 각 키로 사용!
MAC을 통한 무결성 보호
MAC이란?
MAC = Message Authentication Code:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
메시지의 "지문(fingerprint)" 또는 "체크섬"
• 메시지가 변조되었는지 확인
• 키를 사용하므로 위조 불가능
• 암호화와는 별개 (암호화 = 기밀성, MAC = 무결성)
TLS 1.2에서:
HMAC-SHA256 사용 (32바이트)
송신 과정
메시지 송신 시 MAC 생성:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[클라이언트가 "Hello World"를 서버에 보낼 때]
1단계: MAC 계산
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
원본 메시지: "Hello World"
↓
MAC 계산:
↓
MAC = HMAC-SHA256(
key: client_write_MAC_key, ← 세션 키
data: sequence_number + ← 패킷 순서
record_type + ← 레코드 타입
TLS_version + ← TLS 버전
length + ← 길이
"Hello World" ← 실제 메시지
)
구체적 예:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
key: "mac_c_123..." (32바이트)
data:
0x0000000000000001 (sequence = 1)
0x17 (application data)
0x0303 (TLS 1.2)
0x000B (length = 11)
"Hello World"
MAC 결과: "8f3e9d2a..." (32바이트)
2단계: 메시지 + MAC 결합
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────┐
│ 평문 메시지: │
│ "Hello World" │
├─────────────────────────┤
│ MAC: │
│ "8f3e9d2a..." (32바이트)│
└─────────────────────────┘
3단계: 전체 암호화
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AES-256-CBC로 암호화
(client_write_encryption_key 사용)
↓
┌─────────────────────────┐
│ 🔒 암호화된 데이터 │
│ [메시지 + MAC 모두 암호화]│
│ "A7F3B2C9..." │
└─────────────────────────┘
↓
네트워크로 전송
수신 과정
메시지 수신 시 MAC 검증:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[서버가 패킷을 받았을 때]
1단계: 복호화
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────┐
│ 🔒 암호화된 데이터 │
│ "A7F3B2C9..." │
└─────────────────────────┘
↓
AES-256-CBC 복호화
(client_write_encryption_key 사용)
↓
┌─────────────────────────┐
│ 메시지: │
│ "Hello World" │
├─────────────────────────┤
│ 받은 MAC: │
│ "8f3e9d2a..." │
└─────────────────────────┘
2단계: MAC 재계산
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
받은 메시지로 MAC을 다시 계산:
↓
계산된 MAC = HMAC-SHA256(
key: client_write_MAC_key, ← 같은 키!
data: 0x0000000000000001 + ← 같은 sequence
0x17 + 0x0303 + 0x000B +
"Hello World" ← 받은 메시지
)
↓
계산된 MAC: "8f3e9d2a..."
3단계: MAC 비교
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
받은 MAC: "8f3e9d2a..."
계산된 MAC: "8f3e9d2a..."
↓
바이트 단위 비교
↓
일치하는가?
↓
YES → ✓ 무결성 확인
✓ 메시지 사용
✓ sequence 증가
NO → ✗ 변조 감지!
✗ 연결 즉시 종료
✗ fatal alert 전송
변조 공격 시나리오
공격자가 변조 시도:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
시나리오 1: 암호화된 데이터 무작위 변조
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[클라이언트 송신]
암호화: "A7F3B2C9..."
↓
[공격자가 중간에서 변조]
변조: "A7F39999..." (일부 비트 변경)
↓
[서버 수신]
↓
복호화 결과: "H3ll0 W@rld" (깨진 데이터)
받은 MAC: "8f3e9d2a..."
↓
서버가 MAC 재계산:
HMAC-SHA256(key, "H3ll0 W@rld")
↓
계산된 MAC: "2b7f5a1c..." ← 다름!
↓
비교: "8f3e9d2a..." ≠ "2b7f5a1c..."
↓
✗ 불일치! 변조 감지!
↓
[연결 즉시 종료]
시나리오 2: 복호화 후 메시지 변조
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
가정: 공격자가 암호화를 풀 수 있다면?
[공격자가 메시지 변경]
메시지: "Hello World" → "Hello Hacker"
MAC: "8f3e9d2a..." (원본 그대로)
↓
[서버가 검증]
받은 메시지: "Hello Hacker"
받은 MAC: "8f3e9d2a..."
↓
MAC 재계산:
HMAC-SHA256(key, "Hello Hacker")
↓
계산된 MAC: "9a4f2e8b..." ← 다름!
↓
비교: "8f3e9d2a..." ≠ "9a4f2e8b..."
↓
✗ 불일치! 변조 감지!
시나리오 3: 메시지 + MAC 둘 다 변조
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
공격자가 시도:
메시지: "Hello World" → "Hello Hacker"
↓
새로운 MAC 생성 시도:
HMAC-SHA256(???, "Hello Hacker")
↓
문제: MAC 키를 모름!
↓
client_write_MAC_key는:
• 핸드셰이크에서 안전하게 교환됨
• 공개키로 암호화되어 전송됨
• 공격자는 이 키를 알 수 없음
↓
✗ 올바른 MAC 생성 불가능!
↓
[서버가 여전히 탐지함]
시나리오 4: 재전송 공격 (Replay Attack)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
정상 통신:
패킷 1: "Transfer $100" + MAC(seq=1, ...)
패킷 2: "Check balance" + MAC(seq=2, ...)
공격자가 패킷 1을 재전송:
패킷 1 재전송: "Transfer $100" + MAC(seq=1, ...)
↓
[서버 검증]
현재 sequence: 2
받은 sequence: 1
↓
"어? seq=1은 이미 처리했는데?"
↓
✗ 재전송 공격 탐지!
↓
[패킷 폐기]
MAC에 포함되는 추가 정보
MAC 계산 시 포함 데이터:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MAC = HMAC-SHA256(MAC_key, data)
data 구성:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌────────────────────────┐
│ sequence_number │ ← 8바이트, 재전송 공격 방지
│ 0x0000000000000001 │
├────────────────────────┤
│ record_type │ ← 1바이트
│ 0x17 (application) │
├────────────────────────┤
│ TLS_version │ ← 2바이트
│ 0x0303 (TLS 1.2) │
├────────────────────────┤
│ length │ ← 2바이트
│ 0x000B (11 bytes) │
├────────────────────────┤
│ message_content │ ← 실제 메시지
│ "Hello World" │
└────────────────────────┘
→ 이 모든 것을 합쳐서 MAC 계산!
→ 어느 하나라도 바뀌면 MAC 완전히 달라짐
TLS 1.3의 개선 (AEAD)
TLS 1.3: AEAD 통합 방식:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TLS 1.2 (MAC-then-Encrypt):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[메시지] → [MAC 추가] → [전체 암호화] → [전송]
단점:
• 두 단계 필요 (MAC + 암호화)
• 패딩 오라클 공격에 취약 가능
TLS 1.3 (AEAD):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[메시지] → [암호화 + 인증 동시에] → [전송]
AEAD = Authenticated Encryption with Associated Data
알고리즘:
• AES-GCM (Galois/Counter Mode)
• ChaCha20-Poly1305
장점:
✓ 한 번의 연산으로 암호화 + MAC
✓ 더 빠름
✓ 더 안전 (패딩 오라클 공격 불가)
✓ 별도의 MAC 키 불필요
AES-GCM 예시:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
입력:
• 키: encryption_key
• 논스(nonce): 12바이트
• 평문: "Hello World"
• 추가 인증 데이터(AAD): 헤더 정보
출력:
• 암호문: "A7F3B2C9..."
• 인증 태그: "8f3e9d2a..." (16바이트)
→ 하나의 연산으로 암호화 + MAC!